<?php
// +----------------------------------------------------------------------
// | SwiftPHP [ JUST DO ONE THING WELL ]
// +----------------------------------------------------------------------
// | Copyright (c) 2015-2017  http://swiftphp.zhongwenyu.com All rights reserved.
// +----------------------------------------------------------------------
// | Author: zhongwenyu <zhongwenyu1987@163.com> <http://www.zhongwenyu.com>
// +----------------------------------------------------------------------
// | Times: 2017/2/23 21:03
// +----------------------------------------------------------------------
namespace swift\db;

use PDO;
use PDOStatement;
use swift\Config;
use swift\Db;
use swift\Log;
use swift\Request;

abstract class Connection{
    protected $PDOStatement;    // PDO操作实例
    protected $queryStr = '';    // 当前sql指令
    protected $numRows = 0;    // 影响记录的行数
    protected $error = '';    // 错误信息
    protected $links = [];    // 数据库连接ID 支持多个连接
    protected $linkID;    // 当前连接ID
    protected $linkRead;
    protected $linkWrite;
    protected $fetchType = PDO::FETCH_ASSOC;    // 查询结果类型
    protected $attrCase = PDO::CASE_LOWER;    // 字段属性大小写
    protected $query = [];    // 查询对象
    protected $runtime;   // 运行时间
    protected $config = [
        // 数据库类型
        'type'            => '',
        // 服务器地址
        'hostname'        => '',
        // 数据库名
        'database'        => '',
        // 用户名
        'username'        => '',
        // 密码
        'password'        => '',
        // 端口
        'hostport'        => '',
        // 连接dsn
        'dsn'             => '',
        // 数据库连接参数
        'params'          => [],
        // 数据库编码默认采用utf8
        'charset'         => 'utf8',
        // 数据库表前缀
        'prefix'          => '',
        // 数据库调试模式
        'debug'           => false,
        // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
        'deploy'          => 0,
        // 数据库读写是否分离 主从式有效
        'rw_separate'     => false,
        // 读写分离后 主服务器数量
        'master_num'      => 1,
        // 指定从服务器序号
        'slave_no'        => '',
        // 是否严格检查字段是否存在
        'fields_strict'   => true,
        // 数据返回类型
        'result_type'     => PDO::FETCH_ASSOC,
        // 数据集返回类型
        'resultset_type'  => 'array',
        // 自动写入时间戳字段
        'auto_timestamp'  => false,
        // 时间字段取出后的默认时间格式
        'datetime_format' => 'Y-m-d H:i:s',
        // 是否需要进行SQL性能分析
        'sql_explain'     => false,
        // Builder类
        'builder'         => '',
        // Query类
        'query'           => '\\swift\\db\\Query',
        // 是否需要断线重连
        'break_reconnect' => false,
    ];    // 数据库连接参数配置
    protected $bind = [];    // 参数绑定
    protected $params = [    //PDO连接参数
        PDO::ATTR_CASE              => PDO::CASE_NATURAL,
        PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_ORACLE_NULLS      => PDO::NULL_NATURAL,
        PDO::ATTR_STRINGIFY_FETCHES => false,
        PDO::ATTR_EMULATE_PREPARES  => false,
    ];

    /**
     * 架构函数 读取数据库配置信息
     * @access public
     * @param array $config 数据库配置数组
     */
    public function __construct(array $config = []){
        if(!empty($config)){
            $this->config = array_merge($this->config , $config);
        }
    }

    /**
     * 执行查询 返回数据集
     * @access public
     * @param string    $sql sql指令
     * @param array     $bind 参数绑定
     * @param bool      $master 是否在主服务器读操作
     * @param bool      $class 是否返回PDO对象
     * @param string        $sql sql指令
     * @param array         $bind 参数绑定
     * @param boolean       $master 是否在主服务器读操作
     * @param bool          $pdo 是否返回PDO对象
     * @return mixed
     * @throws BindParamException
     * @throws PDOException
     */
    public function query($sql, $bind = [], $master = false, $pdo = false){
        $this->initConnect($master);
        if(!$this->linkID){
            return false;
        }

        // sql语句替换
        if(strpos($sql , '__PREFIX__') !== false){
            $sql = str_replace('__PREFIX__' , $this->config['prefix'] , $sql);
        }

        $this->queryStr = $sql;    // 记录sql语句
        if(!empty($bind)){    // 记录绑定参数
            $this->bind = $bind;
        }
        // 释放前次查询结果
        if(!empty($this->PDOStatement) && $this->PDOStatement->queryString != $sql){
            $this->free();
        }
        Db::$queryTimes++;
        try{
            // 调试开始
            $this->debug(true);
            // 预处理
            if(empty($this->PDOStatement)){
                $this->PDOStatement = $this->linkID->prepare($sql);
            }
            // 是否为存储过程调用
            $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
            // 参数绑定
            if($procedure){
                // 存储过程参数绑定
            }else{
                $this->bindValue($bind);
            }
            // 执行查询
            $this->PDOStatement->execute();
            // 调试结束
            $this->debug(false);
            // 清空
            // 返回结果集
            return $this->getResult($pdo , $procedure);
        }catch(\Exception $e){
            throw $e;
        }
    }

    /**
     * 执行语句
     * @access public
     * @param string        $sql sql指令
     * @param array         $bind 参数绑定
     * @return int
     * @throws BindParamException
     * @throws PDOException
     */
    public function execute($sql, $bind = []){
        $this->initConnect(true);
        if(!$this->linkID){
            return false;
        }

        // sql语句替换
        if(strpos($sql , '__PREFIX__') !== false){
            $sql = str_replace('__PREFIX__' , $this->config['prefix'] , $sql);
        }

        $this->queryStr = $sql;    // 记录sql语句
        if(!empty($bind)){    // 记录绑定参数
            $this->bind = $bind;
        }
        // 释放前次查询结果
        if(!empty($this->PDOStatement) && $this->PDOStatement->queryString != $sql){
            $this->free();
        }
        Db::$executeTimes++;
        try{
            // 调试开始
            $this->debug(true);
            // 预处理
            if(empty($this->PDOStatement)){
                $this->PDOStatement = $this->linkID->prepare($sql);
            }
            // 是否为存储过程调用
            $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
            // 参数绑定
            if($procedure){
                // 存储过程参数绑定
            }elseif(!empty($bind)){
                $this->bindValue($bind);
            }
            // 执行查询
            $this->PDOStatement->execute();
            // 调试结束
            $this->debug(false);
            // 返回结果集
            $this->numRows = $this->PDOStatement->rowCount();
            return $this->numRows;
        }catch(\Exception $e){
            throw $e;
        }
    }

    /**
     * 获得数据集数组
     * @access protected
     * @param bool   $pdo 是否返回PDOStatement
     * @param bool   $procedure 是否存储过程
     * @return array
     */
    protected function getResult($pdo = false, $procedure = false)
    {
        if ($pdo) {
            // 返回PDOStatement对象处理
            return $this->PDOStatement;
        }
        if ($procedure) {
            // 存储过程返回结果
            //return $this->procedure();
        }
        $result        = $this->PDOStatement->fetchAll($this->fetchType);
        $this->numRows = count($result);
        return $result;
    }

    /**
     * 参数绑定
     * 支持 ['name'=>'value','id'=>123] 对应命名占位符
     * 或者 ['value',123] 对应问号占位符
     * @access public
     * @param array $bind 要绑定的参数列表
     * @return void
     * @throws BindParamException
     */
    protected function bindValue(array $bind = []){
        foreach($bind as $key => $val){
            if(strpos($key , '.') !== false){
                $key = str_replace('.','_',$key);
            }
            $param = is_numeric($key) ? $key + 1 : $key;
            $result = $this->PDOStatement->bindValue($param , $val);
            if(!$result){
                throw new \Exception("绑定参数 $param 时发生错误！sql：".$this->getRealSql($this->queryStr , $bind));
            }
        }
    }

    /**
     * 初始化数据库连接
     * @access protected
     * @param boolean $master 是否主服务器
     * @return void
     */
    protected function initConnect($master = true)
    {
        if (!empty($this->config['deploy'])) {
            // 采用分布式数据库
            /*if ($master) {
                if (!$this->linkWrite) {
                    $this->linkWrite = $this->multiConnect(true);
                }
                $this->linkID = $this->linkWrite;
            } else {
                if (!$this->linkRead) {
                    $this->linkRead = $this->multiConnect(false);
                }
                $this->linkID = $this->linkRead;
            }*/
        } elseif (!$this->linkID) {
            // 默认单数据库
            $this->linkID = $this->connect();
        }
    }

    /**
     * 获取最近一次查询的sql语句
     * @access public
     * @return string
     */
    public function getLastSql(){
        return $this->getRealSql($this->queryStr, $this->bind);
    }

    /**
     * 连接数据库方法
     * @access public
     * @param array         $config 连接参数
     * @param integer       $linkNum 连接序号
     * @param array|bool    $autoConnection 是否自动连接主数据库（用于分布式）
     * @return PDO
     * @throws Exception
     */
    public function connect(array $config = [], $linkNum = 0, $autoConnection = false){
        if(!isset($this->links[$linkNum])){
            // 连接配置
            if(empty($config)){
                $config = $this->config;
            }else{
                $config = array($this->config , $config);
            }
            // 连接参数
            if(isset($config['params']) && is_array($config['params'])){
                $params = $config['params'] + $this->params;
            }else{
                $params = $this->params;
            }
            // 记录当前字段属性大小写设置
            $this->attrCase = $params[PDO::ATTR_CASE];
            // 数据返回类型
            if (isset($config['result_type'])) {
                $this->fetchType = $config['result_type'];
            }
            // 建立数据库连接
            try{
                if(empty($config['dsn'])){
                    $config['dsn'] = $this->parseDsn($config);
                }
                // 调试模式下，记录开始时间
                if ($config['debug']) {
                    $startTime = microtime(true);
                }
                // PDO实例
                $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params);
            }catch(\Exception $e){
                throw $e;
            }
            return $this->links[$linkNum];
        }
    }

    /**
     * 根据参数绑定组装最终的SQL语句 便于调试
     * @access public
     * @param string    $sql 带参数绑定的sql语句
     * @param array     $bind 参数绑定列表
     * @return string
     */
    public function getRealSql($sql, array $bind = [])
    {
        foreach ($bind as $key => $val) {
            $value = is_array($val) ? $val[0] : $val;
            $type  = is_array($val) ? $val[1] : PDO::PARAM_STR;
            if (PDO::PARAM_STR == $type) {
                $value = $this->quote($value);
            } elseif (PDO::PARAM_INT == $type) {
                $value = (float) $value;
            }

            // 判断占位符
            $sql = is_numeric($key) ?
                substr_replace($sql, $value, strpos($sql, '?'), 1) :
                str_replace(
                    [':' . $key . ')', ':' . $key . ',', ':' . $key . ' '],
                    [$value . ')', $value . ',', $value . ' '],
                    $sql . ' ');
        }
        return rtrim($sql);
    }

    /**
     * SQL指令安全过滤
     * @access public
     * @param string $str SQL字符串
     * @param bool   $master 是否主库查询
     * @return string
     */
    public function quote($str, $master = true)
    {
        $this->initConnect($master);
        return $this->linkID ? $this->linkID->quote($str) : $str;
    }

    // sql语句性能调试
    public function debug($start){
        if(Config::get('sql_debug') && Log::check(['allow_key' => [Config::get('log_key')]])){
            if($start){
                $this->runtime = microtime(true);
            } else {
                $log = [
                    'ip'      => Request::instance()->getIp(),
                    'time'    => date('Y-m-d H:i:s'),
                    'path'    => Request::instance()->module() . '/' . Request::instance()->controller() . '/' . Request::instance()->action(),
                    'sql'     => $this->getRealSql($this->queryStr , $this->bind),
                    'runtime' => microtime(true) - $this->runtime
                ];
                Log::save($log , 'sql');
            }
        }
    }

    // 释放PDO实例
    private function free(){
        $this->PDOStatement = null;
    }

    /**
     * 调用Query类的查询方法
     * @access public
     * @param string    $method 方法名称
     * @param array     $args 调用参数
     * @return mixed
     */
    public function __call($method, $args)
    {
        return call_user_func_array([$this->getQuery(), $method], $args);
    }

    public function create($table){
        if(!empty($table) && is_string($table)){
            if(strpos($this->config['prefix'] , $table) === false){
                $table = $this->config['prefix'] . $table;
            }
            $class = $this->config['query'];
            $query = new $class($this, '');
            $query->table($table);
            return $query;
        }else{
            throw new \Exception("表名不能为空！");
        }
    }

    /**
     * 创建指定模型的查询对象
     * @access public
     * @param string $model 模型类名称
     * @param string $queryClass 查询对象类名
     * @return Query
     */
    public function getQuery($model = 'db', $queryClass = '')
    {
        if (!isset($this->query[$model])) {
            $class               = $queryClass ?: $this->config['query'];
            $this->query[$model] = new $class($this, 'db' == $model ? '' : $model);
        }
        return $this->query[$model];
    }

    /**
     * 获取数据库的配置参数
     * @access public
     * @param string $config 配置名称
     * @return mixed
     */
    public function getConfig($config = '')
    {
        return $config ? $this->config[$config] : $this->config;
    }

    /**
     * 析构方法
     * @access public
     */
    public function __destruct()
    {
        // 释放查询
        if ($this->PDOStatement) {
            $this->free();
        }
        // 关闭连接
        $this->close();
    }

    /**
     * 关闭数据库（或者重新连接）
     * @access public
     * @return $this
     */
    public function close()
    {
        $this->linkID    = null;
        $this->linkWrite = null;
        $this->linkRead  = null;
        $this->links     = [];
        return $this;
    }

    /**
     * 获取最近插入的ID
     * @access public
     * @param string  $sequence     自增序列名
     * @return string
     */
    public function getLastInsID($sequence = null)
    {
        return $this->linkID->lastInsertId($sequence);
    }

    /**
     * 对返数据表字段信息进行大小写转换出来
     * @access public
     * @param array $info 字段信息
     * @return array
     */
    public function fieldCase($info)
    {
        // 字段大小写转换
        switch ($this->attrCase) {
            case PDO::CASE_LOWER:
                $info = array_change_key_case($info);
                break;
            case PDO::CASE_UPPER:
                $info = array_change_key_case($info, CASE_UPPER);
                break;
            case PDO::CASE_NATURAL:
            default:
                // 不做转换
        }
        return $info;
    }
}